/**
 * 
 */
package com.googlecode.cswish.struts.spring;

import java.beans.Introspector;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.SecondaryTable;
import javax.persistence.Table;
import javax.persistence.Transient;

import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.hibernate.annotations.common.util.StringHelper;
import org.hibernate.cfg.Configuration;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.SimpleValue;
import org.springframework.stereotype.Service;

import com.googlecode.cswish.annotation.GenericType;
import com.googlecode.cswish.annotation.QLFormula;
import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.model.PropertyInfo;
import com.googlecode.cswish.struts.DynamicAction;
import com.googlecode.cswish.struts.Routing;
import com.googlecode.cswish.struts.hibernate.DPropertyInfo;
import com.googlecode.cswish.struts.hibernate.DynamicClassLoaderFactory;
import com.googlecode.cswish.struts.hibernate.DynamicModelInfo;
import com.googlecode.cswish.struts.hibernate.HibernateConfiguration;
import com.googlecode.cswish.struts.hibernate.IDynamic;
import com.googlecode.cswish.struts.hibernate.MockObject;
import com.googlecode.cswish.util.ClassEx;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.FrameConstant;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.util.ClassLoaderUtil;

/**
 * Manager the bean meta data information
 * 
 * @author Jerry.Feng Date: 2008-4-9
 */
@Service
public class BeanIntrospector {
	private static final Logger logger = Logger.getLogger(BeanIntrospector.class);
	
	@Resource
	private HibernateConfiguration hibernateConfiguration;
	
	@Resource
	private DFactory dfactory;
	
	@Resource
	// Don't use 'private', because ObjectFactory reference it
	DynamicClassLoaderFactory dynamicClassLoaderFactory;
	
	@Resource
	private CacheService cacheService;
	
	// use concurrent hashmap because the cache can be changed in the runtime
	protected static Cache<ModelInfo> staticBeanInfos;
	protected static Cache<ModelInfo> dynamicBeanInfos;
	
	protected static final Map<String, List<Class>> classHierarchy = new HashMap<String, List<Class>>();

	private Map<String, String> fuzzyClassNameMapping = new HashMap<String, String>();
	
	private Transient transientInstance;
	
	private static BeanIntrospector instance;
	
	public BeanIntrospector() {
		instance = this;
	}
	
	@SuppressWarnings("unchecked")
	@PostConstruct
	@Transient
	public void initClassHierarchy() {		// use public to avoid the proguard confused this method
		staticBeanInfos = cacheService.newShareCache(ModelInfo.class);
		dynamicBeanInfos = cacheService.newCache(ModelInfo.class);
		
		// init classHierarchy & fuzzyClassNameMapping, the total time spends 0 second
		for (Configuration cfg : hibernateConfiguration.getConfigurations()) {
			Iterator<PersistentClass> iter = cfg.getClassMappings();
			while (iter.hasNext()) {
				PersistentClass persistentClass = iter.next();
				String className = persistentClass.getClassName();
				fuzzyClassNameMapping.put(persistentClass.getNodeName().toLowerCase(), className);
				
				try {
					Class clazz = ClassUtils.getClass(className);
					addClassHierarchy(getSupperClass(clazz), clazz);
				} catch (ClassNotFoundException ex) {
					logger.debug("type is not valid class:" + className);
				}
			}
		}
		
		try {
			transientInstance = this.getClass().getDeclaredMethod("initClassHierarchy").getAnnotation(Transient.class);
		} catch (Exception ex) {
			logger.error("Failed to get the transient annotation", ex);
		}
	}
	
	public String guessFullClassName(String shortName) {
		return fuzzyClassNameMapping.get(shortName);
	}
	
	@SuppressWarnings("unchecked")
	private List<Class> getSupperClass(Class clazz) {
		Class parent = clazz.getSuperclass();
		List<Class> parentClasses = new ArrayList<Class>();
		while (parent != null && parent != Object.class) {
			parentClasses.add(parent);
			parent = parent.getSuperclass();
		}
		return parentClasses;
	}
	
	@SuppressWarnings("unchecked")
	private void addClassHierarchy(List<Class> parentClasses, Class subClass) {
		for (Class parentClass : parentClasses) {
			List<Class> subClasses = classHierarchy.get(parentClass.getName());
			if (subClasses == null) {
				subClasses = new ArrayList<Class>();
				classHierarchy.put(parentClass.getName(), subClasses);
			}
			if (!subClasses.contains(subClass)) {
				subClasses.add(subClass);
			}
		}
	}

	private PersistentClass getPersistentClass(String className) {
		if (hibernateConfiguration != null) {
			return hibernateConfiguration.getClassMapping(className);
		} else {
			return null;
		}
	}
	
	public List<Class> getSubClass(String className) {
		return classHierarchy.get(className);
	}
	
	public boolean isHibernateModel(String className) {
		return getPersistentClass(className) != null;
	}
	
	public ModelInfo getModelInfo(String className) {
		return getModelInfo(className, true);
	}
	
	public String getPropertyType(String className, String nestedProperty) {
		String[] properties = StringUtils.split(nestedProperty, ".");
		String propertyType = className;
		boolean wrongNestedProp = false;
		for (String property : properties) {
			ModelInfo modelInfo = getModelInfo(propertyType);
			if (modelInfo != null) {
				PropertyInfo propInfo = modelInfo.getProperty(property);
				if (propInfo != null) {
					propertyType = propInfo.getGenericType();
				} else {
					wrongNestedProp = true;
				}
			} else {
				wrongNestedProp = true;
			}
		}
		if (wrongNestedProp && logger.isDebugEnabled()) {
			logger.debug("Class " + className + " doesn't contain nested property: " + nestedProperty);
		}
		return wrongNestedProp ? null : propertyType;
	}
	
	public PropertyInfo getPropertyInfo(String className, String nestedProperty) {
		ModelPropertyInfo mp = getModelPropertyInfo(className, nestedProperty);
		return mp == null ? null : mp.propertyInfo;
	}
	
	public PropertyInfo getPropertyInfo(String className, String nestedProperty, boolean containSubClass) {
		ModelPropertyInfo mp = getModelPropertyInfo(className, nestedProperty);
		PropertyInfo pi = mp == null ? null : mp.propertyInfo;
		if (pi == null && containSubClass) {
			List<Class> classes = getSubClass(className);
			if (classes != null && classes.size() > 0) {
				for (Class clazz : classes) {
					pi = getPropertyInfo(clazz.getName(), nestedProperty, containSubClass);
					if (pi != null) {
						break;
					}
				}
			}
		}
		return pi;
	}
	
	public ModelPropertyInfo getModelPropertyInfo(String className, String nestedProperty) {
		if (nestedProperty == null) {
			return null;
		}
		
		String[] properties = StringUtils.split(nestedProperty, ".");
		String propertyType = className;
		PropertyInfo propertyInfo = null;
		ModelInfo modelInfo = null;
		for (String property : properties) {
			modelInfo = getModelInfo(propertyType);
			propertyInfo = modelInfo.getProperty(property);
			if (propertyInfo == null)  {
				break;
			}
			propertyType = propertyInfo.getGenericType();
		}
		if (propertyInfo == null) {
			return null;
		} else {
			ModelPropertyInfo modelPropertyInfo = new ModelPropertyInfo(modelInfo, propertyInfo);
			return modelPropertyInfo;
		}
	}
	
	public boolean hasModel(String className) {
		return getModelInfo(className, false) != null;
	}
	
	public String getDescriptor(String className, String nestedPropertyName) {
		if (nestedPropertyName == null) {
			return className;
		}
		
		String[] propertyNames = StringUtils.split(nestedPropertyName, '.');
		
		String modelName = className;
		Class<?> descriptor = null;
		for (int i = 0; modelName != null && i < propertyNames.length; i++) {
			ModelInfo modelInfo = getModelInfo(modelName);
			if (modelInfo != null) {
				String propertyName = propertyNames[i];
				if (propertyName.charAt(propertyName.length() - 1) == ']')	{// [x]
					propertyName = propertyName.substring(0, propertyName.lastIndexOf('['));
					descriptor = getDescriptor(modelInfo, propertyName);
				} else {
					descriptor = getDescriptor(modelInfo, propertyName);
				}
				if (descriptor != null) {
					modelName = descriptor.getName();
				} else {
					modelName = null;
				}
			}
		}
		return descriptor == null ? null : descriptor.getName();
	}
	
	private Class<?> getDescriptor(ModelInfo modelInfo, String propertyName) {
		PropertyInfo propertyInfo = modelInfo.getProperty(propertyName);
		if (propertyInfo != null) {
			return propertyInfo.getGenericTypeClass();
		}
		return null;
	}
	
	private ModelInfo getBeanInfo(String key) {
		ModelInfo modelInfo = dynamicBeanInfos.get(key);
		if (modelInfo == null) {
			modelInfo = staticBeanInfos.get(key);
		}
		return modelInfo;
	}
	private void putBeanInfo(String key, ModelInfo modelInfo) {
		if (modelInfo.isDynamicClass()) {
			dynamicBeanInfos.put(key, modelInfo);
		} else {
			staticBeanInfos.put(key, modelInfo);
			dynamicBeanInfos.remove(key);
		}
	}
	
	public void clean(String fullClassName, boolean removeSimpleName) {
		// simple name, full name, dynamic name
		if (removeSimpleName) {
			String simpleName = getSimpleName(fullClassName);
			staticBeanInfos.remove(simpleName);
		}
		staticBeanInfos.remove(fullClassName);
		
		String dynaName;
		int index = 0;
		do {
			dynaName = ModelInfo.getDynamicClassName(fullClassName, index++);
		} while (dynamicBeanInfos.remove(dynaName) != null);
	}
	
	/**
	 * name = config.appPackage + simpleName or simpleName 
	 * 
	 * @param name
	 * @return
	 */
	public ModelInfo getModelInfo(String className, boolean autoCreation) {
		ModelInfo modelInfo = getBeanInfo(className);
		if (modelInfo != null || !autoCreation) {
			return modelInfo;
		}
		
		modelInfo = createModelInfo(className, null, true);
		return modelInfo;
	}
	
	private ModelInfo createDynamicActionModelInfo() {
		ActionContext actionContext = ActionContext.getContext();
		DynamicAction dynamicAction = (DynamicAction)actionContext.getValueStack().findValue("action");
		
		if (dynamicAction.getActionName() == null) {
			Map<String, Object> context = actionContext.getActionInvocation().getStack().getContext();
			String actionName = (String) context.get(FrameConstant.STACK_ACTIONNAME);
			String path = actionContext.getActionInvocation().getProxy().getActionName();
			dynamicAction.initialize(actionName, path);
		}
		
		String className = dynamicAction.getActionName();
		ModelInfo modelInfo = getBeanInfo(className);
		if (modelInfo == null) {
			modelInfo = new ModelInfo(null);
			modelInfo.setType(className);
			modelInfo.setSimpleName(getSimpleName(className));
			
			for (String propertyName : dynamicAction.getProperties()) {
				PropertyInfo propertyInfo = new PropertyInfo();
				propertyInfo.setName(propertyName);
				
				Class type = dynamicAction.getPropertyType(propertyName);
				propertyInfo.setType(type.getName());
				Class genericType = dynamicAction.getGenericPropertyType(propertyName);
				propertyInfo.setGenericType(genericType.getName());
				
				modelInfo.addProperty(propertyInfo);
			}
			
			putBeanInfo(modelInfo.getType(), modelInfo);
		}
		return modelInfo;
	}
	
	class PropertyDesc {
		private Method readMethod;
		private Method writeMethod;
		private String name;
		private Class type;
	}
	
	protected ModelInfo createModelInfo(String className, PersistentClass persistentClass, boolean showDebug) {
		// create model information
		String simpleName = getSimpleName(className);
		if (simpleName.equals(className)) {
			className = getModelName(simpleName);
		}

		Class<?> modelClass;
		try {
			// objectFactory.getClassInstance(className);
			modelClass = ClassLoaderUtil.loadClass(className, this.getClass());
		} catch (Throwable t) {
			modelClass = null;
		}
		if (modelClass == null && isAction(className) >= 0 
				|| modelClass != null && DynamicAction.class.isAssignableFrom(modelClass)) {
			return createDynamicActionModelInfo();
		}
		
		
		ModelInfo modelInfo;
		if (modelClass == null || Object.class.equals(modelClass) || Object.class.equals(modelClass.getSuperclass())
				|| modelClass.isInterface()) {
			modelInfo = new ModelInfo(null);
		} else {
			String parentName = modelClass.getSuperclass().getName();
			ModelInfo parentModelInfo = getBeanInfo(parentName);
			if (parentModelInfo == null) {
				if (persistentClass == null) {
					persistentClass = getPersistentClass(className);
				}
				parentModelInfo = createModelInfo(parentName, persistentClass, showDebug);
			}
			modelInfo = new ModelInfo(parentModelInfo);
		}
		
		modelInfo.setType(className);
		modelInfo.setSimpleName(simpleName);
		putBeanInfo(modelInfo.getType(), modelInfo);
		if (isModel(className) >= 0) {
			putBeanInfo(simpleName, modelInfo);
		}
		
		// TODO: Refactor the following codes later
		if (modelClass != null && !modelClass.isInterface()) {
			if (persistentClass == null) {
				persistentClass = getPersistentClass(className);
			}
			updateTableName(modelInfo, modelClass, persistentClass);
			
			Map<String, String> actualTypeArguments = ClassEx.getActualTypeArguments(modelClass);
			Collection<PropertyDesc> propertyDescs = getPropertyDescriptors(modelClass, modelInfo.getParent());
			for (PropertyDesc descriptor : propertyDescs) {
				String propertyName = descriptor.name;
				Class<?> type = descriptor.type;
				if ("class".equals(propertyName) || type == null) {	// skip the property
					continue;
				}
				
				PropertyInfo propertyInfo = new PropertyInfo();
				propertyInfo.setName(propertyName);
				
				propertyInfo.setType(type.getName());
				Method readMethod = descriptor.readMethod;
				Method writeMethod = descriptor.writeMethod;
				if (readMethod != null) {
					GenericType sign = readMethod.getAnnotation(GenericType.class);
					if (sign != null) {
						// the priority of hbm is higher 
						propertyInfo.setGenericType(sign.value());
					} else {
						try {
							Type retType = readMethod.getGenericReturnType();
							if (retType == readMethod.getReturnType()) {
								propertyInfo.setGenericType(readMethod.getReturnType().getName());
							} else {
								// TODO: use the new method to improve the performance
								//String genericType = ClassEx.getGenericType(modelClass, retType);
								String genericType = ClassEx.getGenericType(retType, actualTypeArguments);
								if (genericType != null) {
									propertyInfo.setGenericType(genericType);
								}
							}
						} catch(Throwable t) {
						}
					}
				}
				
				propertyInfo.setReadMethod(readMethod);
				propertyInfo.setWriteMethod(writeMethod);
				
				updateTableProperty(modelInfo.getTable(), propertyInfo, null, persistentClass);
				boolean isParentProperty = modelInfo.getProperty(propertyInfo.getName()) != null;
				modelInfo.addProperty(propertyInfo);
				
				if (!isParentProperty) {
					// init the refernce property
					if (!propertyInfo.isTableColumn()) {
						if (readMethod != null) {
							QLFormula qlFormula = readMethod.getAnnotation(QLFormula.class);
							if (qlFormula != null) {
								String targetPropertyName = qlFormula.value();
								
								int index = targetPropertyName.lastIndexOf('.');
								if (index == -1) {
									modelInfo.addRefProperty(targetPropertyName, propertyInfo.getName());
								} else {
									PropertyInfo targetPropertyInfo = getPropertyInfo(className, targetPropertyName.substring(0, index));
									if (targetPropertyInfo != null) {
										String target = targetPropertyName.substring(index + 1);
										String refBy = className + "." + propertyInfo.getName();
										getModelInfo(targetPropertyInfo.getGenericType()).addRefProperty(target, refBy);
									}
								}
							}
						}
					}
				}
			}//for each descriptors
		}
		
		return modelInfo;
	}
	
	private Collection<PropertyDesc> getPropertyDescriptors(Class modelClass, ModelInfo parent) {
		Map<String, PropertyDesc> propertyDescs = new HashMap<String, PropertyDesc>();
		
		Method[] declaredMethods = modelClass.getDeclaredMethods();
		if (parent != null) {
			Collection<PropertyInfo> propertyInfos = parent.getAllProperties();
			for (PropertyInfo propertyInfo : propertyInfos) {
				// use new PropertyDesc for these generic type method, such as public T getXXX();
				if (propertyInfo.hasExtendPropertyType() && !propertyDescs.containsKey(propertyInfo.getName())) {
					Class genericType = propertyInfo.findPropertyType(modelClass);
					if (genericType != null && !genericType.getName().equals(propertyInfo.getGenericType())) {
						// the child class override the parent generic type, use a new PropertyInfo to store it
						PropertyDesc propertyDesc = new PropertyDesc();
						propertyDesc.name = propertyInfo.getName();
						propertyDesc.type = genericType;
						propertyDesc.readMethod = propertyInfo.getReadMethod();
						propertyDesc.writeMethod = propertyInfo.getWriteMethod();
						propertyDescs.put(propertyDesc.name, propertyDesc);
					}
				}
			}
		}
		
		populatePropertyDesc(propertyDescs, declaredMethods);
		
		return propertyDescs.values();
	}

	private void populatePropertyDesc(Map<String, PropertyDesc> propertyDescs, Method[] declaredMethods) {
		for (Method method : declaredMethods) {
			if (!Modifier.isPublic(method.getModifiers())) {
				continue;
			}
			
			boolean isGet = false;
			String methodName = method.getName();
			String propertyName = null;
			if (methodName.startsWith("get") && methodName.length() > 3 && method.getParameterTypes().length == 0) {
				isGet = true;
				propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
			} else if (methodName.startsWith("is") && methodName.length() > 2 && method.getParameterTypes().length == 0) {
				isGet = true;
				propertyName = Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3);
			} else if (methodName.startsWith("set") && methodName.length() > 3 && method.getParameterTypes().length == 1) {
				propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
			}
			
			if (propertyName != null) {
				PropertyDesc propertyDesc = propertyDescs.get(propertyName);
				if (propertyDesc == null) {
					propertyDesc = new PropertyDesc();
					propertyDesc.name = propertyName;
					propertyDescs.put(propertyName, propertyDesc);
				}
				
				if (isGet) {
					propertyDesc.readMethod = method;
					if (propertyDesc.type == null) {
						propertyDesc.type = method.getReturnType();		// TODO: use ClassEx.getGenericType(modelClass, method.getReturnType())?
					}
				} else {
					if (propertyDesc.writeMethod == null) {
						propertyDesc.writeMethod = method;
					} else {
						// because of the polymorphism, the class can have multiple setter method
						// get the matched type according to the getter method
						if (propertyDesc.readMethod == null) {
							// find the read method
							String capName = StringUtils.capitalize(propertyDesc.name);
							String readMethodName = "get" + capName;
							String readMethodName2 = "is" + capName;
							for (Method theMethod : declaredMethods) {
								boolean nameMatched = readMethodName.equals(theMethod.getName()) || readMethodName2.equals(theMethod.getName());
								if (nameMatched && theMethod.getParameterTypes().length == 0) {
									propertyDesc.readMethod = theMethod;
									break;
								}
							}
						}
						// matched method
						if (propertyDesc.readMethod != null && propertyDesc.readMethod.getReturnType().equals(method.getParameterTypes()[0])) {
							propertyDesc.writeMethod = method;
							propertyDesc.type = method.getParameterTypes()[0];
						}
					}
					
					
					if (propertyDesc.type == null) {
						propertyDesc.type = method.getParameterTypes()[0];	// TODO: use ClassEx.getGenericType?
					}
				}
			}
		}
	}

	protected PropertyInfo addPropertyToModelInfo(ModelInfo modelInfo, String name, boolean tableColumn, String type, String genericType) {
		PropertyInfo propertyInfo = new PropertyInfo();
		propertyInfo.setName(name);
		propertyInfo.setType(type);
		propertyInfo.setGenericType(genericType);
		propertyInfo.setDynamic(true);
		propertyInfo.setNullable(true);
		propertyInfo.setInsertable(true);
		propertyInfo.setUpdatable(true);
		propertyInfo.setTableColumn(tableColumn);
		propertyInfo.setColumnName(name);
		modelInfo.addProperty(propertyInfo);
		return propertyInfo;
	}
	
	public DynamicModelInfo getDynamicModelInfo(String className) {
		ModelInfo modelInfo = getModelInfo(className);
		if (!modelInfo.isDynamicClass()) {
			return null;
		}
		
		DynamicModelInfo dynamicModelInfo = new DynamicModelInfo();
		dynamicModelInfo.setDynamic(true);
		String modelName = modelInfo.getType();
		if (modelInfo.getParent() != null) {
			modelName = modelInfo.getParent().getType();			// use the static name
		}
		dynamicModelInfo.setModelName(modelName);
		
		Map<String, DPropertyInfo> properties = new HashMap<String, DPropertyInfo>();
		for (PropertyInfo propertyInfo : modelInfo.getAllProperties()) {
			if (propertyInfo.isDynamic()) {
				DPropertyInfo dpropertyInfo = new DPropertyInfo();
				dpropertyInfo.name = propertyInfo.getName();
				dpropertyInfo.type = propertyInfo.getType();
				dpropertyInfo.genericType = propertyInfo.getGenericType();
				//dpropertyInfo.i18nKey							// TODO: search the Model & ModelField
				dpropertyInfo.transientProperty = !propertyInfo.isTableColumn();
				properties.put(dpropertyInfo.name, dpropertyInfo);
			}
		}
		dynamicModelInfo.setProperties(properties);
		return dynamicModelInfo;
	}
	
	/**
	 * 1. update the property info to ModelInfo<br>
	 * 2. generate the new class if there's new property added<p>
	 * 
	 * @param className
	 * @param newProperties
	 * @param removedProperties
	 * @param genericProperties
	 */
	public void convertModelToDynamicModel(Map<String, DynamicModelInfo> dModelInfos) {
		if (dModelInfos == null || dModelInfos.size() == 0) {
			return;
		}
		
		// check each model, only the sub class can convert
		Map<String, DynamicModelInfo> convertModelInfos = new HashMap<String, DynamicModelInfo>(dModelInfos.size() * 4 / 3);
		for (DynamicModelInfo dModelInfo : dModelInfos.values()) {
			String className = dModelInfo.getModelName();
			List<Class> subClass = getSubClass(className);
			if (subClass != null && subClass.size() > 0) {
				for (Class sub : subClass) {
					DynamicModelInfo subDModelInfo = new DynamicModelInfo(sub.getName(), dModelInfo.getProperties());
					subDModelInfo.setRemovedProperties(dModelInfo.getRemovedProperties());
					convertModelInfos.put(sub.getName(), subDModelInfo);
				}
			} else {
				DynamicModelInfo existed = convertModelInfos.put(className, dModelInfo);
				if (existed != null) {
					// merge the existed model information (which comes from the parent definition)
					dModelInfo.merge(existed);
				}
			}
		}
		
		List<Class<?>> newClasses = new ArrayList<Class<?>>();
		for (DynamicModelInfo dModelInfo : convertModelInfos.values()) {
			String className = dModelInfo.getModelName();
			Map<String, DPropertyInfo> newProperties = dModelInfo.getProperties();
			Collection<String> removedProperties = dModelInfo.getRemovedProperties() == null 
					? null : dModelInfo.getRemovedProperties().keySet();
			
			if (ModelInfo.isDynamicClass(className)) {
				className = ModelInfo.getStaticClassName(className);
			}
			
			// 1. update modelinfo
			ModelInfo modelInfo = getModelInfo(className);
			if (!modelInfo.isDynamicClass()) {
				ModelInfo parent = modelInfo;
				modelInfo = new ModelInfo(parent);
				modelInfo.setType(className);			// temporary name
				modelInfo.setGenericType(parent.getType());
				modelInfo.setTable(parent.getTable());
				modelInfo.setSimpleName(parent.getSimpleName());
			}
			// new property
			for (Entry<String, DPropertyInfo> entry : newProperties.entrySet()) {
				// add new property to current class
				// override it in the child class event if the parent defined this property
				PropertyInfo propertyInfo = modelInfo.getDeclaredProperty(entry.getKey());
				if (propertyInfo == null) {
					PropertyInfo parentPropertyInfo = modelInfo.getProperty(entry.getKey());
					DPropertyInfo dpropertyInfo = entry.getValue();
					boolean isSame = false;
					if (parentPropertyInfo != null) {
						if (parentPropertyInfo.isTableColumn() == !dpropertyInfo.transientProperty
								&& parentPropertyInfo.getType().equals(dpropertyInfo.type)
								&& parentPropertyInfo.getGenericType().equals(dpropertyInfo.genericType)) {
							isSame = true;
						}
					}
					if (!isSame) {
						addPropertyToModelInfo(modelInfo, entry.getKey(), !dpropertyInfo.transientProperty,
							dpropertyInfo.type, dpropertyInfo.genericType);
					}
				}
			}
			// removed property
			if (removedProperties != null && removedProperties.size() > 0) {
				for (String property : removedProperties) {
					modelInfo.removeProperty(property);
				}
			}
			
			// 2.1 check if there's new property
			boolean needChangeClass = false;
			Map<String, PropertyDesc> propertyDescs = new HashMap<String, PropertyDesc>();
			Class currentModelClass = modelInfo.getTypeClass();
			if (currentModelClass != null) {
				Class parentClass = currentModelClass;
				while (parentClass != null && !parentClass.equals(Object.class)){
					populatePropertyDesc(propertyDescs, parentClass.getDeclaredMethods());
					parentClass = parentClass.getSuperclass();
				}
				for (String newProperty : newProperties.keySet()) {
					if (!propertyDescs.containsKey(newProperty)) {
						needChangeClass = true;
						break;
					}
				}
			} else {
				// Class not found, it's totally new VO class
				needChangeClass = true;
			}
			if (!needChangeClass && removedProperties != null) {
				boolean isDynamicClass = currentModelClass == null || ModelInfo.isDynamicClass(currentModelClass.getName());
				if (isDynamicClass) {
					for (String removedProperty : removedProperties) {
						if (propertyDescs.containsKey(removedProperty)) {
							needChangeClass = true;
							break;
						}
					}
				}
			}
			
			String dynamicClassName;
			if (needChangeClass) {
				// 2.2 generate a new class
				Class clazz = defineDynamicClass(modelInfo);
				newClasses.add(clazz);
				dynamicClassName = clazz.getName();
			} else {
				dynamicClassName = currentModelClass == null ? modelInfo.getType() : currentModelClass.getName();
			}
			
			// 2.7 add the dynamicClassName as an alias name
			modelInfo.setType(dynamicClassName);
			modelInfo.setGenericType(className);
			
			putBeanInfo(dynamicClassName, modelInfo);
			putBeanInfo(getSimpleName(dynamicClassName), modelInfo);
			putBeanInfo(className, modelInfo);
			putBeanInfo(getSimpleName(className), modelInfo);
			boolean isDynamicClass = ModelInfo.isDynamicClass(dynamicClassName);
			dModelInfo.setDynamic(isDynamicClass);
			
			if (needChangeClass) {
				if (logger.isDebugEnabled()) {
					logger.debug("Convert " + className + " to dynamic class " + dynamicClassName);
				}
			}
		}
		
		for (Class clazz : newClasses) {
			ModelInfo modelInfo = getModelInfo(clazz.getName());
			String className = modelInfo.getGenericType();
			DynamicModelInfo dModelInfo = convertModelInfos.get(className);
			Map<String, DPropertyInfo> newProperties = dModelInfo.getProperties();
			
			// 2.6 update the new class information to modelInfo (only the dynamic property)
			Method[] methods = clazz.getDeclaredMethods();
			for (Method method : methods) {
				String name = method.getName();
				boolean isSetter = name.startsWith("set");
				String property = Introspector.decapitalize(name.substring(3));
				PropertyInfo propertyInfo = modelInfo.getDeclaredProperty(property);
				if (propertyInfo == null) {
					DPropertyInfo dpropertyInfo = newProperties.get(property);
					propertyInfo = addPropertyToModelInfo(modelInfo, property, !dpropertyInfo.transientProperty,
							dpropertyInfo.type, dpropertyInfo.genericType);
				}
				if (isSetter) {
					propertyInfo.setWriteMethod(method);
				} else {
					propertyInfo.setReadMethod(method);
				}
			}
		}
	}

	private Class defineDynamicClass(ModelInfo modelInfo) {
		String className = modelInfo.getType();
		dynamicClassLoaderFactory.unloadClass(modelInfo.getType());
		
		// 2.3 get all the dynamic property
		List<NameType> dynaProperties = new ArrayList<NameType>();
		for (PropertyInfo propertyInfo : modelInfo.getAllProperties()) {
			if (propertyInfo.isDynamic()) {
				NameType nameType = new NameType();
				nameType.name = propertyInfo.getName();
				
				// check if the type is dynamic class
				String type = propertyInfo.getType();
				if (propertyInfo.isDynamic() && !ModelInfo.isDynamicClass(type)) {
					Class clazz = ClassUtil.forName(type);
					if (clazz == null) {
						type = ModelInfo.getDynamicClassName(type, 0);
					}
				}
				nameType.type = type;
				
				if (!propertyInfo.isTableColumn()) {
					nameType.annotations = new Annotation[] {transientInstance};
				}
				dynaProperties.add(nameType);
			}
		}
		
		// 2.4 find a new name
		String newClassName = className;
		Class clazz = null;
		String staticClassName = ModelInfo.isDynamicClass(className) ? 
				ModelInfo.getStaticClassName(className) : className;
		int index = 0;
		do {
			try {
				clazz = null;
				newClassName = ModelInfo.getDynamicClassName(staticClassName, index++);
				dynamicClassLoaderFactory.findClass(newClassName);
			} catch (ClassNotFoundException ex) {
			}
		} while(clazz != null);
		
		// 2.5 create it
		ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
		Class superClass;
		try {
			superClass = Class.forName(staticClassName);
		} catch (ClassNotFoundException e) {
			// define a blank parent class
			ClassLoader loader = dynamicClassLoaderFactory.getClassLoader(staticClassName, true);
			try {
				Thread.currentThread().setContextClassLoader(loader);
				superClass = dfactory.defineVO(staticClassName, MockObject.class, null, Collections.EMPTY_LIST);
			} finally {
				Thread.currentThread().setContextClassLoader(oldLoader);
			}
		}
		dynamicClassLoaderFactory.unloadClass(newClassName);
		ClassLoader loader = dynamicClassLoaderFactory.getClassLoader(newClassName, true);
		try {
			Thread.currentThread().setContextClassLoader(loader);
			clazz = dfactory.defineVO(newClassName, superClass, new Class[] {IDynamic.class}, dynaProperties);
		} finally {
			Thread.currentThread().setContextClassLoader(oldLoader);
		}
		return clazz;
	}
	
	protected void updateTableProperty(String tableName, PropertyInfo propertyInfo, Property property, PersistentClass persistentClass) {
		int index = 0;
		if (property == null && persistentClass != null) {
			// find the property and the position index
			String propertyName = propertyInfo.getName();
			Property identifierProperty = persistentClass.getIdentifierProperty();
			if (identifierProperty != null
					&& identifierProperty.getName().equals(StringHelper.root(propertyName))
					) {
				property = identifierProperty;
			} else {
				Iterator iterator = persistentClass.getPropertyClosureIterator();
				while (iterator.hasNext() ) {
					index++;
					Property prop = (Property) iterator.next();
					if (prop.getName().equals(StringHelper.root(propertyName) ) ) {
						property = prop;
						break;
					}
				}
			}
		}
		
		if (property != null) {
			propertyInfo.setTableColumn(true);
			Iterator columnIter = property.getColumnIterator();
			if (columnIter.hasNext()) {
				Object object = columnIter.next();
				if (object instanceof org.hibernate.mapping.Column) {
					org.hibernate.mapping.Column column = (org.hibernate.mapping.Column)object;
					
					boolean isId = persistentClass.getIdentifierProperty() == property;
					if (isId) {
						SimpleValue idValue = (SimpleValue) property.getValue();
						String generatorStrategy = idValue.getIdentifierGeneratorStrategy();
						boolean autoId = !"assigned".equals(generatorStrategy);
						propertyInfo.setAutoId(autoId);
					}
					propertyInfo.setColumnName(column.getName()); 
					// 255 is not the real length of DB column
					if (column.getLength() != org.hibernate.mapping.Column.DEFAULT_LENGTH) {
						propertyInfo.setLength(column.getLength());
					}
					propertyInfo.setUnique(column.isUnique());
					propertyInfo.setNullable(column.isNullable());
				}
			} else {
				propertyInfo.setColumnName(property.getName());
			}
			propertyInfo.setInsertable(property.isInsertable());
			propertyInfo.setUpdatable(property.isUpdateable());
		} else {
			// TODO: check the field annotations?
			Method method = propertyInfo.getReadMethod();
			if (method != null) {
				Column column = method.getAnnotation(Column.class);
				boolean isTransient = tableName == null
					&& method.getDeclaringClass().getAnnotation(Embeddable.class) == null
					|| method.getAnnotation(Transient.class) != null;
				propertyInfo.setTableColumn(!isTransient);
				if (column != null) {
					if (StringUtils.isNotEmpty(column.table())) {
						propertyInfo.setTableName(column.table());
					}
					if (StringUtils.isNotEmpty(column.name())) {
						propertyInfo.setColumnName(column.name());
					}
					
					// 255 is not the real length of DB column
					if (column.length() != org.hibernate.mapping.Column.DEFAULT_LENGTH) {
						propertyInfo.setLength(column.length());
					}
					propertyInfo.setUnique(column.unique());
					propertyInfo.setNullable(column.nullable());
					propertyInfo.setInsertable(column.insertable());
					propertyInfo.setUpdatable(column.updatable());
				} else {
					if (!isTransient) {
						JoinColumn joinColumn = method.getAnnotation(JoinColumn.class);
						if (joinColumn != null) {
							if (StringUtils.isNotEmpty(joinColumn.table())) {
								propertyInfo.setTableName(joinColumn.table());
							}
							if (StringUtils.isNotEmpty(joinColumn.name())) {
								propertyInfo.setColumnName(joinColumn.name());
							}
						}
						
						propertyInfo.setNullable(true);
						propertyInfo.setInsertable(true);
						propertyInfo.setUpdatable(true);
					}
				}
			}
		}
	}
	
	protected void updateTableName(ModelInfo modelInfo, Class<?> modelClass, PersistentClass persistentClass) {
		String tableName = null;
		if (persistentClass != null) {
			tableName = persistentClass.getTable().getName();
		} else {
			if (modelClass != null) {
				Table table = modelClass.getAnnotation(Table.class);
				if (table != null) {
					tableName = table.name();
				} else {
					Entity entity = modelClass.getAnnotation(Entity.class);
					if (entity != null) {
						tableName = entity.name();
						if (tableName.length() == 0) {
							tableName = StringUtils.uncapitalize(modelClass.getSimpleName());
						}
					}
				}
				
				SecondaryTable secondaryTable = modelClass.getAnnotation(SecondaryTable.class);
				if (secondaryTable != null) {
					modelInfo.setSecondTable(secondaryTable.name());
				}
			}
		}
		
		modelInfo.setTable(tableName);
	}
	
	
	
	/**
	 * Check a class if it's a model
	 * 
	 * @param className
	 * @return
	 */
	public int isModel(String className) {
		if (className == null) {
			return -1;
		}
		
		// TODO: is there better way to check the model type?
		if (FrameConstant.MODEL_PACKAGE_SUFFIX.isEmpty()) {
			if (className.startsWith(FrameConstant.PACKAGE_PREFIX) 
					&& isAction(className) < 0 && isService(className) < 0) {
				return 0;
			} else {
				return -1;
			}
		} else {
			return getMatchedIndex(className, FrameConstant.PACKAGE_PREFIX, FrameConstant.MODEL_PACKAGE_SUFFIX);
		}
	}
	
	/**
	 * Check a class if it's a action
	 * 
	 * @param className
	 * @return
	 */
	public int isAction(String className) {
		if (className == null) {
			return -1;
		}
		return getMatchedIndex(className, FrameConstant.PACKAGE_PREFIX, FrameConstant.ACTION_PACKAGE_SUFFIX);
	}
	/**
	 * Check a class if it's a service
	 * 
	 * @param className
	 * @return
	 */
	public int isService(String className) {
		if (className == null) {
			return -1;
		}
		return getMatchedIndex(className, FrameConstant.PACKAGE_PREFIX, FrameConstant.SERVICE_PACKAGE_SUFFIX);
	}

	private int getMatchedIndex(String className, String packages,
			String packageSuffixs) {
		if (className.startsWith(packages) && className.endsWith(packageSuffixs)) {
			return 0;
		}
		return -1;
	}
	
	/**
	 * Get the simple name according to naming rules:
	 * <li> Action = Action package + simple name + method + suffix
	 * <li> Service = Application package + simple name + suffix
	 * <li> Model = Model package + simple name + suffix
	 * 
	 * @param actionName
	 * @return
	 */
	public String getSimpleName(String className) {
		String simpleName;
		if (isAction(className) >= 0) {
			String theName = className.substring(FrameConstant.PACKAGE_PREFIX.length(),
					className.length() - FrameConstant.ACTION_PACKAGE_SUFFIX.length());
			int separator = Routing.findMethodSeparator(theName);
			
			if (separator > 0) {
				simpleName = theName.substring(0, separator);
			} else {
				simpleName = theName;
			}
		} else if (isService(className) >= 0) {
			simpleName = className.substring(FrameConstant.PACKAGE_PREFIX.length(),
					className.length() -FrameConstant.SERVICE_PACKAGE_SUFFIX.length());
		} else if (isModel(className) >= 0) {
			simpleName = className.substring(FrameConstant.PACKAGE_PREFIX.length(),
					className.length() - FrameConstant.MODEL_PACKAGE_SUFFIX.length());
		} else {
			simpleName = className;
		}
		
		int separator = simpleName.lastIndexOf('.') + 1;
		if (simpleName.length() > 0 && Character.isUpperCase(simpleName.charAt(separator))) {
			simpleName = simpleName.substring(0, separator)
				+ Character.toLowerCase(simpleName.charAt(separator))
				+ simpleName.substring(separator + 1);
		}
		return simpleName;
	}
	
	/**
	 * Get model name according to the simple name
	 * 
	 * @param simpleName
	 * @return
	 */
	public String getModelName(String simpleName) {
		if (simpleName == null || isModel(simpleName) >= 0) {
			return simpleName;
		} else {
			String modelPackage = FrameConstant.PACKAGE_PREFIX;
			return buildClassName(modelPackage, simpleName, FrameConstant.MODEL_PACKAGE_SUFFIX);
		}
	}

	private String buildClassName(String packageName, String simpleName, String suffix) {
		int index = simpleName.lastIndexOf('.') + 1;
		if (Character.isLowerCase(simpleName.charAt(index))) {
			return packageName + simpleName.substring(0, index)
				+ Character.toUpperCase(simpleName.charAt(index))
				+ simpleName.substring(index + 1) + suffix;
		} else {
			return packageName + simpleName + suffix;
		}
	}
	
	/**
	 * Get action name according to the simple name, simpleName + . + methodName
	 * 
	 * @param simpleName
	 * @return
	 */
	public String getActionName(String simpleName) {
		if (isAction(simpleName) > 0) {
			return simpleName;
		} else {
			String actionPackage = FrameConstant.PACKAGE_PREFIX;
			return buildClassName(actionPackage, simpleName, FrameConstant.ACTION_PACKAGE_SUFFIX);
		}
	}
	
	public String getActionName(String simpleName, String method) {
		return FrameConstant.PACKAGE_PREFIX + simpleName + '.' 
			+ StringUtils.capitalize(method) + FrameConstant.ACTION_PACKAGE_SUFFIX;
	}
	
	/**
	 * Get action name according to the simple name
	 * 
	 * @param simpleName
	 * @return
	 */
	public String getServiceName(String serviceName) {
//		if (serviceName.lastIndexOf('.') == -1) {
//			return guessFullClassName(serviceName) + FrameConstant.SERVICE_PACKAGE_SUFFIX;
//		} 
		if (isService(serviceName) > 0) {
			return serviceName;
		} else {
			String servicePackage = FrameConstant.PACKAGE_PREFIX;
			return buildClassName(servicePackage, serviceName, FrameConstant.SERVICE_PACKAGE_SUFFIX);
		}
	}

	public static BeanIntrospector getInstance() {
		return instance;
	}

	public void convertToStaticClass(String className) {
		ModelInfo modelInfo = getBeanInfo(className);
		List<String> removedProperties = new ArrayList<String>();
		if (modelInfo != null && modelInfo.isDynamicClass()) {
			for (PropertyInfo propertyInfo : 
				new ArrayList<PropertyInfo>(modelInfo.getDeclaredProperties())) {
				if (propertyInfo.isDynamic()) {
					modelInfo.removeProperty(propertyInfo.getName());
					removedProperties.add(propertyInfo.getName());
				}
			}
			
			// the dynamic model extends the static model or it's new dynamic model totally
			if (modelInfo.getParent() == null) {
				dynamicBeanInfos.remove(modelInfo.getType());
				String simpleName = getSimpleName(modelInfo.getType());
				dynamicBeanInfos.remove(simpleName);
			} else {
				putBeanInfo(className, modelInfo.getParent());
			}
		}
		String simpleName = getSimpleName(className);
		putBeanInfo(simpleName, modelInfo);
		
		removeSubClassProperties(className, removedProperties);
		
		if (logger.isDebugEnabled()) {
			logger.debug("Convert " + className + " to static class");
		}
	}
	
	private void removeSubClassProperties(String className, List<String> removedProperties) {
		List<Class> subClass = getSubClass(className);
		if (subClass != null) {
			for (Class sub : subClass) {
				ModelInfo subModelInfo = getModelInfo(sub.getName());
				
				if (subModelInfo.isDynamicClass()) {
					// remove the dynamic property
					for (String removedProperty : removedProperties) {
						subModelInfo.removeProperty(removedProperty);
					}
					// check if it can convert to static class
					boolean hasDynamic = false;
					for (PropertyInfo info : subModelInfo.getDeclaredProperties()) {
						if (info.isDynamic()) {
							hasDynamic = true;
							break;
						}
					}
					if (!hasDynamic) {		// it's static class
						if (subModelInfo.getParent() == null) {
							dynamicBeanInfos.remove(sub.getName());
						} else {
							putBeanInfo(sub.getName(), subModelInfo.getParent());
						}
					}
				}
				
				// scan its children
				String theSubClassName = getModelName(subModelInfo.getSimpleName());
				removeSubClassProperties(theSubClassName, removedProperties);
			}
		}
	}
	
	public void unloadDynamicModel(String className) {	
		// remove all the dynamic class key & unload the class
		for (int i = 0; i < 100; i++) {
			String dynamicName = ModelInfo.getDynamicClassName(className, i);
			ModelInfo dmodelInfo = getBeanInfo(dynamicName);
			if (dmodelInfo == null) {
				break;
			}
			
			dynamicBeanInfos.remove(dynamicName);
			dynamicBeanInfos.remove(getSimpleName(dynamicName));
			
			dynamicClassLoaderFactory.unloadClass(dynamicName);
		}
		
		// check the sub class
		List<Class> subClass = getSubClass(className);
		if (subClass != null) {
			for (Class sub : subClass) {
				ModelInfo subModelInfo = getModelInfo(sub.getName());
				
				boolean hasDynamic = false;
				for (PropertyInfo info : subModelInfo.getDeclaredProperties()) {
					if (info.isDynamic()) {
						hasDynamic = true;
						break;
					}
				}
				
				if (!hasDynamic) {
					unloadDynamicModel(sub.getName());
				}
			}
		}
	}
}
